Merged [20921], [20922], [20969], [20971]: Added MySpace service and account for...
[adiumx.git] / Plugins / Purple Service / ESPurpleJabberAccount.m
blob72b577b8a93788327d4dbc68f1cebe0360f0d5da
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
7  * or (at your option) any later version.
8  * 
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
11  * Public License for more details.
12  * 
13  * You should have received a copy of the GNU General Public License along with this program; if not,
14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
15  */
17 #import "ESPurpleJabberAccount.h"
18 #import <AdiumLibpurple/SLPurpleCocoaAdapter.h>
19 #import <Adium/AIAccountControllerProtocol.h>
20 #import <Adium/AIInterfaceControllerProtocol.h>
21 #import <Adium/AIStatusControllerProtocol.h>
22 #import <Adium/AIContactControllerProtocol.h>
23 #import <Adium/AIChat.h>
24 #import <Adium/AIHTMLDecoder.h>
25 #import <Adium/AIListContact.h>
26 #import <Adium/AIStatus.h>
27 #import <Adium/ESFileTransfer.h>
28 #import <Adium/ESTextAndButtonsWindowController.h>
29 #import <AIUtilities/AIAttributedStringAdditions.h>
30 #include <Libpurple/buddy.h>
31 #include <Libpurple/presence.h>
32 #include <Libpurple/si.h>
34 #define DEFAULT_JABBER_HOST @"@jabber.org"
36 extern void jabber_roster_request(JabberStream *js);
38 @implementation ESPurpleJabberAccount
39         
40 /*!
41  * @brief The UID will be changed. The account has a chance to perform modifications
42  *
43  * Upgrade old Jabber accounts stored with the host in a separate key to have the right UID, in the form
44  * name@server.org
45  *
46  * Append @jabber.org to a proposed UID which has no domain name and does not need to be updated.
47  *
48  * @param proposedUID The proposed, pre-filtered UID (filtered means it has no characters invalid for this servce)
49  * @result The UID to use; the default implementation just returns proposedUID.
50  */
51 - (NSString *)accountWillSetUID:(NSString *)proposedUID
53         proposedUID = [proposedUID lowercaseString];
54         NSString        *correctUID;
55         
56         if ((proposedUID && ([proposedUID length] > 0)) && 
57            ([proposedUID rangeOfString:@"@"].location == NSNotFound)) {
58                 
59                 NSString        *host;
60                 //Upgrade code: grab a previously specified Jabber host
61                 if ((host = [self preferenceForKey:@"Jabber:Host" group:GROUP_ACCOUNT_STATUS ignoreInheritedValues:YES])) {
62                         //Determine our new, full UID
63                         correctUID = [NSString stringWithFormat:@"%@@%@",proposedUID, host];
65                         //Clear the preference and then set the UID so we don't perform this upgrade again
66                         [self setPreference:nil forKey:@"Jabber:Host" group:GROUP_ACCOUNT_STATUS];
67                         [self setPreference:correctUID forKey:@"FormattedUID" group:GROUP_ACCOUNT_STATUS];
69                 } else {
70                         //Append [self serverSuffix] (e.g. @jabber.org) to a Jabber account with no server
71                         correctUID = [proposedUID stringByAppendingString:[self serverSuffix]];
72                 }
73         } else {
74                 correctUID = proposedUID;
75         }
77         return correctUID;
80 - (const char*)protocolPlugin
82    return "prpl-jabber";
85 - (NSSet *)supportedPropertyKeys
87         static NSMutableSet *supportedPropertyKeys = nil;
88         
89         if (!supportedPropertyKeys) {
90                 supportedPropertyKeys = [[NSMutableSet alloc] initWithObjects:
91                         @"AvailableMessage",
92                         @"Invisible",
93                         nil];
94                 [supportedPropertyKeys unionSet:[super supportedPropertyKeys]];
95         }
96         
97         return supportedPropertyKeys;
100 - (void)configurePurpleAccount
102         [super configurePurpleAccount];
103         
104         NSString        *connectServer;
105         BOOL            forceOldSSL, allowPlaintext;
107         purple_account_set_username(account, [self purpleAccountName]);
109         //'Connect via' server (nil by default)
110         connectServer = [self preferenceForKey:KEY_JABBER_CONNECT_SERVER group:GROUP_ACCOUNT_STATUS];
111         //XXX - As of libpurple 2.0.0, 'localhost' doesn't work properly by 127.0.0.1 does. Hack!
112         if (connectServer && [connectServer isEqualToString:@"localhost"])
113                 connectServer = @"127.0.0.1";
114         
115         purple_account_set_string(account, "connect_server", (connectServer ?
116                                                                                                                 [connectServer UTF8String] :
117                                                                                                                 ""));
118         
119         //Force old SSL usage? (off by default)
120         forceOldSSL = [[self preferenceForKey:KEY_JABBER_FORCE_OLD_SSL group:GROUP_ACCOUNT_STATUS] boolValue];
121         purple_account_set_bool(account, "old_ssl", forceOldSSL);
123         //Allow plaintext authorization over an unencrypted connection? Purple will prompt if this is NO and is needed.
124         allowPlaintext = [[self preferenceForKey:KEY_JABBER_ALLOW_PLAINTEXT group:GROUP_ACCOUNT_STATUS] boolValue];
125         purple_account_set_bool(account, "auth_plain_in_clear", allowPlaintext);
128 - (NSString *)serverSuffix
130         AILog(@"using jabber");
131         return DEFAULT_JABBER_HOST;
134 /*!     @brief  Obtain the resource name for this Jabber account.
136  *      This could be extended in the future to perform keyword substitution (e.g. s/%computerName%/CSCopyMachineName()/).
138  *      @return The resource name for the account.
139  */
140 - (NSString *)resourceName
142         return [self preferenceForKey:KEY_JABBER_RESOURCE group:GROUP_ACCOUNT_STATUS];
145 - (const char *)purpleAccountName
147         NSString        *userNameWithHost = nil, *completeUserName = nil;
148         BOOL            serverAppendedToUID;
149         
150         /*
151          * Purple stores the username in the format username@server/resource.  We need to pass it a username in this format
152          *
153          * The user should put the username in username@server format, which is common for Jabber. If the user does
154          * not specify the server, use jabber.org.
155          */
156         
157         serverAppendedToUID = ([UID rangeOfString:@"@"].location != NSNotFound);
158         
159         if (serverAppendedToUID) {
160                 userNameWithHost = UID;
161         } else {
162                 userNameWithHost = [UID stringByAppendingString:[self serverSuffix]];
163         }
165         completeUserName = [NSString stringWithFormat:@"%@/%@" ,userNameWithHost, [self resourceName]];
167         return [completeUserName UTF8String];
171  * @brief Connect Host
173  * Convenience method for retrieving the connect host for this account
175  * Rather than having a separate server field, Jabber uses the servername after the user name.
176  * username@server.org
178  * The connect server, stored in KEY_JABBER_CONNECT_SERVER, overrides this to provide the connect host. It will
179  * not be set in most cases.
180  */
181 - (NSString *)host
183         NSString        *host;
184         
185         if (!(host = [self preferenceForKey:KEY_JABBER_CONNECT_SERVER group:GROUP_ACCOUNT_STATUS])) {
186                 int location = [UID rangeOfString:@"@"].location;
188                 if ((location != NSNotFound) && (location + 1 < [UID length])) {
189                         host = [UID substringFromIndex:(location + 1)];
191                 } else {
192                         host = [self serverSuffix];
193                 }
194         }
195         
196         return host;
200  * @brief Should set aliases serverside?
202  * Jabber supports serverside aliases.
203  */
204 - (BOOL)shouldSetAliasesServerside
206         return YES;
209 - (AIListContact *)contactWithUID:(NSString *)sourceUID
211         AIListContact   *contact;
212         
213         contact = [[adium contactController] existingContactWithService:service
214                                                                                                                         account:self
215                                                                                                                                 UID:sourceUID];
216         if (!contact) {         
217                 contact = [[adium contactController] contactWithService:[self _serviceForUID:sourceUID]
218                                                                                                                 account:self
219                                                                                                                         UID:sourceUID];
220         }
221         
222         return contact;
225 - (AIService *)_serviceForUID:(NSString *)contactUID
227         AIService       *contactService;
228         NSString        *contactServiceID = nil;
230         if ([contactUID hasSuffix:@"@gmail.com"] ||
231                 [contactUID hasSuffix:@"@googlemail.com"]) {
232                 contactServiceID = @"libpurple-jabber-gtalk";
234         } else if([contactUID hasSuffix:@"@livejournal.com"]){
235                 contactServiceID = @"libpurple-jabber-livejournal";
236                 
237         } else {
238                 contactServiceID = @"libpurple-Jabber";
239         }
241         contactService = [[adium accountController] serviceWithUniqueID:contactServiceID];
242         
243         return contactService;
246 #pragma mark Contacts
247 - (void)updateSignon:(AIListContact *)theContact withData:(void *)data
249         [super updateSignon:theContact withData:data];
250         
251         //We only get user icons in Jabber when we request info. Do that now!
252         [self delayedUpdateContactStatus:theContact];
255 #pragma mark Status
257 - (NSString *)encodedAttributedString:(NSAttributedString *)inAttributedString forListObject:(AIListObject *)inListObject
259         static AIHTMLDecoder *jabberHtmlEncoder = nil;
260         if (!jabberHtmlEncoder) {
261                 jabberHtmlEncoder = [[AIHTMLDecoder alloc] init];
262                 [jabberHtmlEncoder setIncludesHeaders:NO];
263                 [jabberHtmlEncoder setIncludesFontTags:YES];
264                 [jabberHtmlEncoder setClosesFontTags:YES];
265                 [jabberHtmlEncoder setIncludesStyleTags:YES];
266                 [jabberHtmlEncoder setIncludesColorTags:YES];
267                 [jabberHtmlEncoder setEncodesNonASCII:NO];
268                 [jabberHtmlEncoder setPreservesAllSpaces:NO];
269                 [jabberHtmlEncoder setUsesAttachmentTextEquivalents:YES];
270         }
271         
272         return [jabberHtmlEncoder encodeHTML:inAttributedString imagesPath:nil];
275 - (NSString *)_UIDForAddingObject:(AIListContact *)object
277         NSString        *objectUID = [object UID];
278         NSString        *properUID;
279         
280         if ([objectUID rangeOfString:@"@"].location != NSNotFound) {
281                 properUID = objectUID;
282         } else {
283                 properUID = [NSString stringWithFormat:@"%@@%@",objectUID,[self host]];
284         }
285         
286         return [properUID lowercaseString];
289 - (NSString *)unknownGroupName {
290     return (AILocalizedString(@"Roster","Roster - the Jabber default group"));
293 - (NSString *)connectionStringForStep:(int)step
295         switch (step) {
296                 case 0:
297                         return AILocalizedString(@"Connecting",nil);
298                         break;
299                 case 1:
300                         return AILocalizedString(@"Initializing Stream",nil);
301                         break;
302                 case 2:
303                         return AILocalizedString(@"Reading data",nil);
304                         break;                  
305                 case 3:
306                         return AILocalizedString(@"Authenticating",nil);
307                         break;
308                 case 5:
309                         return AILocalizedString(@"Initializing Stream",nil);
310                         break;
311                 case 6:
312                         return AILocalizedString(@"Authenticating",nil);
313                         break;
314         }
315         return nil;
318 - (BOOL)shouldAttemptReconnectAfterDisconnectionError:(NSString **)disconnectionError
320         BOOL shouldReconnect = YES;
321         
322         if (disconnectionError && *disconnectionError) {
323                 if (([*disconnectionError rangeOfString:@"401"].location != NSNotFound) ||
324                         ([*disconnectionError rangeOfString:@"Authentication Failure"].location != NSNotFound) ||
325                         ([*disconnectionError rangeOfString:@"Not Authorized"].location != NSNotFound)) {
326                         shouldReconnect = NO;
328                         /* Automatic registration attempt */
329                         //Display no error message
330                         [*disconnectionError release];
331                         *disconnectionError = nil;
333                         [[adium interfaceController] displayQuestion:AILocalizedString(@"Would you like to register a new Jabber account?", nil)
334                                                                                  withDescription:AILocalizedString(@"Jabber was unable to connect due to an invalid Jabber ID or password.  This may be because you do not yet have an account on this Jabber server.  Would you like to register now?",nil)
335                                                                                  withWindowTitle:AILocalizedString(@"Invalid Jabber ID or Password",nil)
336                                                                                    defaultButton:AILocalizedString(@"Register",nil)
337                                                                                  alternateButton:AILocalizedString(@"Cancel",nil)
338                                                                                          otherButton:nil
339                                                                                                   target:self
340                                                                                                 selector:@selector(answeredShouldReigsterNewJabberAccount:userInfo:)
341                                                                                                 userInfo:nil];
343                 } else if ([*disconnectionError rangeOfString:@"Stream Error"].location != NSNotFound) {
344                         shouldReconnect = NO;
346                 } else if ([*disconnectionError rangeOfString:@"requires plaintext authentication over an unencrypted stream"].location != NSNotFound) {
347                         shouldReconnect = NO;
348                         
349                 } else if ([*disconnectionError rangeOfString:@"Resource Conflict"].location != NSNotFound) {
350                         shouldReconnect = NO;
351                 }
352         }
353         
354         return shouldReconnect;
357 - (BOOL)answeredShouldReigsterNewJabberAccount:(NSNumber *)returnCodeNumber userInfo:(id)userInfo
359         AITextAndButtonsReturnCode returnCode = [returnCodeNumber intValue];
361         switch (returnCode) {
362                 case AITextAndButtonsDefaultReturn:
363                         [self performSelector:@selector(performRegisterWithPassword:)
364                                            withObject:password
365                                            afterDelay:1];
366                         break;
368                 case AITextAndButtonsAlternateReturn:
369                 case AITextAndButtonsOtherReturn:
370                 case AITextAndButtonsClosedWithoutResponse:
371                         [self serverReportedInvalidPassword];
372                         break;
373         }
374         
375         return YES;
378 - (void)disconnectFromDroppedNetworkConnection
380         /* Before we disconnect from a dropped network connection, set gc->disconnect_timeout to a non-0 value.
381          * This will let the prpl know that we are disconnecting with no backing ssl connection and that therefore
382          * the ssl connection is has should not be messaged in the process of disconnecting.
383          */
384         PurpleConnection *gc = purple_account_get_connection(account);
385         if (PURPLE_CONNECTION_IS_VALID(gc) &&
386                 !gc->disconnect_timeout) {
387                 gc->disconnect_timeout = -1;
388                 AILog(@"%@: Disconnecting from a dropped network connection", self);
389         }
391         [super disconnectFromDroppedNetworkConnection];
394 #pragma mark File transfer
395 - (BOOL)canSendFolders
397         return NO;
400 - (void)beginSendOfFileTransfer:(ESFileTransfer *)fileTransfer
402         [super _beginSendOfFileTransfer:fileTransfer];
405 - (void)acceptFileTransferRequest:(ESFileTransfer *)fileTransfer
407     [super acceptFileTransferRequest:fileTransfer];    
410 - (void)rejectFileReceiveRequest:(ESFileTransfer *)fileTransfer
412     [super rejectFileReceiveRequest:fileTransfer];    
415 - (void)cancelFileTransfer:(ESFileTransfer *)fileTransfer
417         [super cancelFileTransfer:fileTransfer];
420 #pragma mark Status Messages
421 - (NSAttributedString *)statusMessageForPurpleBuddy:(PurpleBuddy *)b
423         NSAttributedString  *statusMessage = nil;
425         if (purple_account_is_connected(account)) {             
426                 char    *normalized = g_strdup(purple_normalize(b->account, b->name));
427                 JabberBuddy     *jb;
428                 
429                 if ((jb = jabber_buddy_find(account->gc->proto_data, normalized, FALSE))) {
430                         NSString        *statusMessageString = nil;
431                         const char      *msg = jabber_buddy_get_status_msg(jb);
432                         
433                         if (msg) {
434                                 //Get the custom jabber status message if one is set
435                                 statusMessageString = [NSString stringWithUTF8String:msg];
436                         }
437                         
438                         if (statusMessageString && [statusMessageString length]) {
439                                 statusMessage = [AIHTMLDecoder decodeHTML:statusMessageString];
440                         }
441                 }
442                 
443                 g_free(normalized);
444         }
445         
446         return statusMessage;
449 - (NSString *)statusNameForPurpleBuddy:(PurpleBuddy *)buddy
451         NSString                *statusName = nil;
452         PurplePresence  *presence = purple_buddy_get_presence(buddy);
453         PurpleStatus            *status = purple_presence_get_active_status(presence);
454         const char              *purpleStatusID = purple_status_get_id(status);
455         
456         if (!purpleStatusID) return nil;
458         if (!strcmp(purpleStatusID, jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_CHAT))) {
459                 statusName = STATUS_NAME_FREE_FOR_CHAT;
460                 
461         } else if (!strcmp(purpleStatusID, jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_XA))) {
462                 statusName = STATUS_NAME_EXTENDED_AWAY;
463                 
464         } else if (!strcmp(purpleStatusID, jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_DND))) {
465                 statusName = STATUS_NAME_DND;
466                 
467         }
468         
469         return statusName;
473  * @brief Jabber status messages are plaintext
474  */
475 - (NSString *)encodedAttributedString:(NSAttributedString *)inAttributedString forStatusState:(AIStatus *)statusState
477         return [[inAttributedString attributedStringByConvertingLinksToStrings] string];
480 #pragma mark Menu items
481 - (NSString *)titleForContactMenuLabel:(const char *)label forContact:(AIListContact *)inContact
483         if (strcmp(label, "Un-hide From") == 0) {
484                 return [NSString stringWithFormat:AILocalizedString(@"Un-hide From %@",nil),[inContact formattedUID]];
486         } else if (strcmp(label, "Temporarily Hide From") == 0) {
487                 return [NSString stringWithFormat:AILocalizedString(@"Temporarily Hide From %@",nil),[inContact formattedUID]];
489         } else if (strcmp(label, "Unsubscribe") == 0) {
490                 return [NSString stringWithFormat:AILocalizedString(@"Unsubscribe %@",nil),[inContact formattedUID]];
492         } else if (strcmp(label, "(Re-)Request authorization") == 0) {
493                 return [NSString stringWithFormat:AILocalizedString(@"Re-request Authorization from %@",nil),[inContact formattedUID]];
495         } else if (strcmp(label,  "Cancel Presence Notification") == 0) {
496                 return [NSString stringWithFormat:AILocalizedString(@"Cancel Presence Notification to %@",nil),[inContact formattedUID]];       
497         }
498         
499         return [super titleForContactMenuLabel:label forContact:inContact];
502 #pragma mark Multiuser chat
504 //Multiuser chats come in with just the contact's name as contactName, but we want to actually do it right.
505 - (NSString *)uidForContactWithUID:(NSString *)inUID inChat:(AIChat *)chat
507         return [NSString stringWithFormat:@"%@/%@",[chat name],inUID];
510 #pragma mark Status
512  * @brief Return the purple status type to be used for a status
514  * Most subclasses should override this method; these generic values may be appropriate for others.
516  * Active services provided nonlocalized status names.  An AIStatus is passed to this method along with a pointer
517  * to the status message.  This method should handle any status whose statusNname this service set as well as any statusName
518  * defined in  AIStatusController.h (which will correspond to the services handled by Adium by default).
519  * It should also handle a status name not specified in either of these places with a sane default, most likely by loooking at
520  * [statusState statusType] for a general idea of the status's type.
522  * @param statusState The status for which to find the purple status ID
523  * @param arguments Prpl-specific arguments which will be passed with the state. Message is handled automatically.
525  * @result The purple status ID
526  */
527 - (const char *)purpleStatusIDForStatus:(AIStatus *)statusState
528                                                         arguments:(NSMutableDictionary *)arguments
530         const char              *statusID = NULL;
531         NSString                *statusName = [statusState statusName];
532         NSString                *statusMessageString = [statusState statusMessageString];
533         NSNumber                *priority = nil;
534         
535         if (!statusMessageString) statusMessageString = @"";
537         switch ([statusState statusType]) {
538                 case AIAvailableStatusType:
539                 {
540                         if (([statusName isEqualToString:STATUS_NAME_FREE_FOR_CHAT]) ||
541                            ([statusMessageString caseInsensitiveCompare:[[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_FREE_FOR_CHAT]] == NSOrderedSame))
542                                 statusID = jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_CHAT);
543                         priority = [self preferenceForKey:KEY_JABBER_PRIORITY_AVAILABLE group:GROUP_ACCOUNT_STATUS];
544                         break;
545                 }
546                         
547                 case AIAwayStatusType:
548                 {
549                         if (([statusName isEqualToString:STATUS_NAME_DND]) ||
550                            ([statusMessageString caseInsensitiveCompare:[[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_DND]] == NSOrderedSame))
551                                 statusID = jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_DND);
552                         else if (([statusName isEqualToString:STATUS_NAME_EXTENDED_AWAY]) ||
553                                          ([statusMessageString caseInsensitiveCompare:[[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_EXTENDED_AWAY]] == NSOrderedSame))
554                                 statusID = jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_XA);
555                         priority = [self preferenceForKey:KEY_JABBER_PRIORITY_AWAY group:GROUP_ACCOUNT_STATUS];
556                         break;
557                 }
558                         
559                 case AIInvisibleStatusType:
560                         AILog(@"Warning: Invisibility is not yet supported in libpurple 2.0.0 jabber");
561                         priority = [self preferenceForKey:KEY_JABBER_PRIORITY_AWAY group:GROUP_ACCOUNT_STATUS];
562                         statusID = jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_AWAY);
563 //                      statusID = "Invisible";
564                         break;
565                         
566                 case AIOfflineStatusType:
567                         break;
568         }
570         //Set our priority, which is actually set along with the status...Default is 0.
571         [arguments setObject:(priority ? priority : [NSNumber numberWithInt:0])
572                                   forKey:@"priority"];
574         //If we didn't get a purple status ID, request one from super
575         if (statusID == NULL) statusID = [super purpleStatusIDForStatus:statusState arguments:arguments];
576         
577         return statusID;
580 #pragma mark Account Action Menu Items
581 - (NSString *)titleForAccountActionMenuLabel:(const char *)label
583         /* XXX All Jabber account actions depend upon adiumPurpleRequestFields */
584         return nil;
587 @end